package nl.grol.yakshop.domain.model;

import java.util.List;
import java.util.logging.Logger;

import nl.grol.yakshop.util.ConversionUtil;

import org.joda.time.DateTime;

import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;

/**
 * @author mgrol
 *
 */

/*
 * Invisible outside package:
 * Labyak shall only be accessed through its interface
 */
class LabYak implements YakI {
    private static final Logger LOGGER = Logger.getLogger(LabYak.class.getName());

	/*
	 * Lobyak specific constants
	 */
	private static final int DAYS_PER_YAK_YEAR = 100;
	private static final int MAXIMUM_AGE_IN_DAYS = 10 * DAYS_PER_YAK_YEAR;
	private static final int MINIMUM_AGE_FOR_SHAVING_IN_DAYS = 100;
	private static final int MINIMUM_AGE_FOR_MILKING_IN_DAYS = 0;
	private static final long MSEC_PER_DAY  = 24 * 60 * 60 * 1000;
	
	private String name;
	private DateTime birthDate; 
	private Gender gender;
	private List<Integer> shavingSchedule = null;

	/*
	 * Can not be instantiated directly by business logic
	 *   Use the factoy to instantiate a Yak
	 */
	LabYak(String name, double currentAgeInYakYears, Gender gender) {
		super();
		
		/*
		 * Verify preconditions
		 */
		Preconditions.checkArgument(name != null);
		Preconditions.checkArgument(currentAgeInYakYears >= 0, 
				"currentAgeInYakYears: received: %s expected: >= 0", currentAgeInYakYears );
		Preconditions.checkArgument(currentAgeInYakYears < MAXIMUM_AGE_IN_DAYS, 
				"currentAgeInYakYears: received: %s expected: < %s", 
							currentAgeInYakYears, MAXIMUM_AGE_IN_DAYS );
		Preconditions.checkArgument(gender != null);
		
		this.name = name;
		int ageInYakDays = (int)(currentAgeInYakYears * DAYS_PER_YAK_YEAR); // round floor
		this.birthDate = DateTime.now().minusDays(ageInYakDays);
		this.gender = gender;
		
		/*
		 * Create a list of days (=since shop-opening) on which this particular
		 * yak must be shaven. 
		 * The list is later used to:
		 *   - determine if a yak must be shaved on a particular day.
		 *   - determine last-shaved age
		 */
		this.shavingSchedule = createShavingScheduleForYak();

	}

	private List<Integer> createShavingScheduleForYak( ) {
		List<Integer> shavingScheduleForYak = Lists.newArrayList();
		
		// Start  at shop-opening day
		int aDay = 0;
		while( this.isAliveOnDay(aDay) == true ) { 
			// loop breaks when yak has reached age of death
			
			int ageOnDay = this.getAgeInDaysOnDay(aDay);
			if( ageOnDay < LabYak.MINIMUM_AGE_FOR_SHAVING_IN_DAYS) {
				/*
				 * The animal is not  yet old enough to be shaven on this day.
				 * Let's try again for tomorrow.
				 */
				aDay++;
			} else {
				/*
				 * Store this day as a "shaving"-day in the list for this yak
				 */
				shavingScheduleForYak.add(  Integer.valueOf(aDay) );
				
				/*
				 * Determine days to wait before the yak can be shaven again: 
				 * Note: the age of the yak at the current shave determines the next shave.
				 */
//				LOGGER.finest("Interval 1: " + (8 + (ageOnDay * 0.01)));
//				LOGGER.finest("Interval 2: " + (((int)(8 + (ageOnDay * 0.01))) + 1) );
				int daysUntillNextShaving = ((int)(8 + (ageOnDay * 0.01))) + 1;

				/*
				 * Continue loop with next shaving day
				 */
				aDay += daysUntillNextShaving;
			}
		}
		
		return shavingScheduleForYak;
	}


	@Override
	public String getName() {
		return name;
	}

	@Override
	public DateTime getBirthDate() {
		return this.birthDate;
	}
	
	@Override
	public Gender getGender() {
		return gender;
	}

	@Override
	public int getAgeInDaysOnDay( int daysSinceStartOfShop) {
		
		Preconditions.checkArgument(daysSinceStartOfShop >= 0, 
				"getAgeInDaysOnDay: received: %s expected: >= 0", daysSinceStartOfShop );

		long deltaMsec = DateTime.now().getMillis() - this.birthDate.getMillis();
		int ageIndays = ((int)(deltaMsec/MSEC_PER_DAY)) + daysSinceStartOfShop;
		
		return ageIndays;
	}

	@Override
	public double getAgeInYearsOnDay( int daysSinceStartOfShop ) {

		int ageInDays = getAgeInDaysOnDay(daysSinceStartOfShop);
		
		// force into double before devision
		return ((double)ageInDays) / DAYS_PER_YAK_YEAR; 
	}
		

	@Override
	public boolean isAliveOnDay( int daysSinceStartOfShop ) {
		
		Preconditions.checkArgument(daysSinceStartOfShop >= 0, 
				"isAliveOnDay: received: %s expected: >= 0", daysSinceStartOfShop );

		/*
		 * Upper boundary exclusive
		 */
		boolean status = this.getAgeInDaysOnDay(daysSinceStartOfShop) < MAXIMUM_AGE_IN_DAYS;
		
		return status;
	}

	/*
	 * Methods related to Milkable
	 */
	@Override
	public boolean isOldEnoughToBeMilkedOnDay( int daysSinceStartOfShop ) {
		boolean status = false;
		
		Preconditions.checkArgument(daysSinceStartOfShop >= 0, 
				"isOldEnoughToBeMilkedOnDay: received: %s expected: >= 0", daysSinceStartOfShop );

		if( this.isAliveOnDay(daysSinceStartOfShop) == true )  {
			/*
			 * Lower boundary is inclusive
			 */
			status = this.getAgeInDaysOnDay(daysSinceStartOfShop) >= MINIMUM_AGE_FOR_MILKING_IN_DAYS;
		}
		
		return status;
	}
	
	@Override
	public double getMilkProductionOnDay( int daysSinceStartOfShop ) {
		double milkProductionForDay = 0.0;

		Preconditions.checkArgument(daysSinceStartOfShop >= 0, 
				"getMilkProductionOnDay: received: %s expected: >= 0", daysSinceStartOfShop );

		if( this.isOldEnoughToBeMilkedOnDay(daysSinceStartOfShop) == true ) {
			/*
			 * Use formula as provided in specification
			 */
			milkProductionForDay = 
				50 - ( this.getAgeInDaysOnDay(daysSinceStartOfShop ) * 0.03 );
		}

		return milkProductionForDay;
	}

	/*
	 * Methods related to Shaveable
	 */
	@Override
	public boolean isOldEnoughToBeShavedOnDay( int daysSinceStartOfShop ) {
		boolean status = false;
		
		Preconditions.checkArgument(daysSinceStartOfShop >= 0, 
				"isOldEnoughToBeShavedOnDay: received: %s expected: >= 0", daysSinceStartOfShop );

		if( this.isAliveOnDay(daysSinceStartOfShop) == true )  {
			/*
			 * Lower boundary is inclusive
			 */
			status = this.getAgeInDaysOnDay(daysSinceStartOfShop) >= MINIMUM_AGE_FOR_SHAVING_IN_DAYS;
		}
		
		return status;
	}

	@Override
	public boolean mustBeShavedOnDay( int daysSinceStartOfShop ) {
		boolean mustBeShavenOnDay = false;
		
		Preconditions.checkArgument(daysSinceStartOfShop >= 0, 
				"mustBeShavenOnDay: received: %s expected: >= 0", daysSinceStartOfShop );
		
		/*
		 * Try to find current day in prepared shaving schedule list
		 */
		if( this.shavingSchedule.contains(Integer.valueOf(daysSinceStartOfShop)) == true ) {
			mustBeShavenOnDay = true;
		}

		return mustBeShavenOnDay;
	}

	@Override
	public double getLastShavedAgeInYearsOnDay(int daysSinceStartOfShop) {
		double lastShavedAge;

		Preconditions.checkArgument(daysSinceStartOfShop >= 0, 
				"getLastShavedAgeInYearsOnDay: received: %s expected: >= 0", daysSinceStartOfShop );

		if(this.shavingSchedule.size() == 0 ) {
			lastShavedAge = -1.0;
		} else {
			
			/*
			 * Minus one because shaving same does not count
			 */
			int lastDaySchaved = findDayOrDayBeforeInSchedule(daysSinceStartOfShop-1, 
									this.shavingSchedule);
			
			/*
			 * Convert to age from days to yak-years
			 */
			lastShavedAge = getAgeInYearsOnDay(lastDaySchaved);
			//log.info( "lastShavedAge:" + lastShavedAge );
			
		}
		return lastShavedAge;
	}
	
	private int findDayOrDayBeforeInSchedule( int dayToFind, List<Integer> schedule ) {
		int lastDayShaved = -1;

		if( dayToFind < 0 ) {
			dayToFind = 0;
		}
		
		//LOGGER.finest("Find: t=" + dayToFind + " in shaving-schedule:" + scheduleAsString() );
		
		/*
		 * Lookup this day or the day-before in schedule
		 */
		int previous = -1;
		for( Integer aDayInSchedule : schedule ) {
			if( aDayInSchedule > dayToFind ) {
				// we are one too far, so take previous and we are done
				lastDayShaved = previous;
				break;
			} else {
				// try next entry in schedule
				previous = aDayInSchedule.intValue();
			}
		}
		
		if( lastDayShaved < 0 ) {
			/*
			 * Too large for schedule: Use last value from schedule
			 */
			lastDayShaved = this.shavingSchedule.get(this.shavingSchedule.size()-1);
		//	LOGGER.finest( "Value "+ dayToFind + " is too large for schedule " + 
			//		" Use last from schedule " + lastDayShaved);
		}

		return lastDayShaved;
	}

	private String scheduleAsString() {
		String scheduleString = null;
		if( this.shavingSchedule != null ) {
			StringBuffer sb = new StringBuffer();
			for( Integer d : this.shavingSchedule ) {
				sb.append( d + "," );
			}
			scheduleString = sb.toString();
		} else {
			scheduleString = "?";
		}
		return scheduleString;
	}

	public String toString() {
		
		return Objects.toStringHelper(this)
	       .add("name", this.name)
	       .add("birthDate", this.birthDate)
	       .add("gender", this.gender)
	       .add("initial-ageInDays", this.getAgeInDaysOnDay(0))
	       .add("initial-ageInYakYears", ConversionUtil.format2(this.getAgeInYearsOnDay(0)))
	       .add("initial-isAlive", this.isAliveOnDay(0))
	       .add("initial-isMilkable", this.isOldEnoughToBeMilkedOnDay(0))
	       .add("initial-isShaveable", this.isOldEnoughToBeShavedOnDay(0))
	       .add("\nshaving-schedule", scheduleAsString())
	       .toString();
	}
}
